पाइथन की unittest.mock लाइब्रेरी में महारत हासिल करें। मजबूत यूनिट टेस्टिंग के लिए टेस्ट डबल्स, मॉक ऑब्जेक्ट्स, और पैच डेकोरेटर का गहन विश्लेषण।
पाइथन मॉक ऑब्जेक्ट्स: टेस्ट डबल कार्यान्वयन के लिए एक व्यापक गाइड
आधुनिक सॉफ्टवेयर विकास की दुनिया में, कोड लिखना केवल आधी लड़ाई है। यह सुनिश्चित करना कि कोड विश्वसनीय, मजबूत और अपेक्षा के अनुरूप काम करता है, दूसरा, उतना ही महत्वपूर्ण आधा हिस्सा है। यहीं पर स्वचालित परीक्षण (automated testing) काम आता है। यूनिट टेस्टिंग, विशेष रूप से, एक मौलिक अभ्यास है जिसमें किसी एप्लिकेशन के अलग-अलग घटकों या 'यूनिट्स' को आइसोलेशन में टेस्ट करना शामिल है। हालांकि, यह आइसोलेशन अक्सर कहना आसान होता है लेकिन करना मुश्किल। वास्तविक दुनिया के एप्लिकेशन आपस में जुड़े ऑब्जेक्ट्स, सेवाओं और बाहरी सिस्टम का एक जटिल जाल होते हैं। आप एक सिंगल फंक्शन का परीक्षण कैसे कर सकते हैं यदि यह डेटाबेस, किसी तीसरे पक्ष के एपीआई, या आपके सिस्टम के किसी अन्य जटिल हिस्से पर निर्भर करता है?
इसका उत्तर एक शक्तिशाली तकनीक में निहित है: टेस्ट डबल्स (Test Doubles) का उपयोग। और पाइथन इकोसिस्टम में, उन्हें बनाने का प्राथमिक उपकरण बहुमुखी और अपरिहार्य unittest.mock लाइब्रेरी है। यह गाइड आपको पाइथन में मॉक्स और टेस्ट डबल्स की दुनिया में गहराई से ले जाएगी। हम उनके पीछे के 'क्यों' का पता लगाएंगे, विभिन्न प्रकारों को स्पष्ट करेंगे, और आपको स्वच्छ, तेज और अधिक प्रभावी टेस्ट लिखने में मदद करने के लिए unittest.mock का उपयोग करके व्यावहारिक, वास्तविक दुनिया के उदाहरण प्रदान करेंगे।
टेस्ट डबल्स क्या हैं और हमें उनकी आवश्यकता क्यों है?
कल्पना कीजिए कि आप एक ऐसा फ़ंक्शन बना रहे हैं जो आपकी कंपनी के डेटाबेस से उपयोगकर्ता की प्रोफ़ाइल प्राप्त करता है और फिर उसे प्रारूपित करता है। फ़ंक्शन सिग्नेचर कुछ इस तरह दिख सकता है: get_formatted_user_profile(user_id, db_connection)।
इस फ़ंक्शन का परीक्षण करने के लिए, आपको कई चुनौतियों का सामना करना पड़ता है:
- लाइव सिस्टम पर निर्भरता: आपके टेस्ट के लिए एक चालू डेटाबेस की आवश्यकता होगी। यह टेस्ट को धीमा, सेट अप करने में जटिल और बाहरी सिस्टम की स्थिति और उपलब्धता पर निर्भर बना देता है।
- अप्रत्याशितता: डेटाबेस में डेटा बदल सकता है, जिससे आपका टेस्ट विफल हो सकता है, भले ही आपका फॉर्मेटिंग लॉजिक सही हो। यह टेस्ट को 'फ्लेकी' या गैर-नियतात्मक (non-deterministic) बनाता है।
- एज केस का परीक्षण करने में कठिनाई: आप यह कैसे परीक्षण करेंगे कि यदि डेटाबेस कनेक्शन विफल हो जाता है, या यदि यह एक ऐसे उपयोगकर्ता को लौटाता है जिसमें कुछ डेटा गायब है तो क्या होता है? एक वास्तविक डेटाबेस के साथ इन विशिष्ट परिदृश्यों का अनुकरण करना अविश्वसनीय रूप से कठिन हो सकता है।
एक टेस्ट डबल किसी भी ऑब्जेक्ट के लिए एक सामान्य शब्द है जो एक टेस्ट के दौरान एक वास्तविक ऑब्जेक्ट की जगह लेता है। वास्तविक db_connection को एक टेस्ट डबल से बदलकर, हम वास्तविक डेटाबेस पर निर्भरता को समाप्त कर सकते हैं और परीक्षण के माहौल पर पूरा नियंत्रण रख सकते हैं।
टेस्ट डबल्स का उपयोग करने से कई प्रमुख लाभ मिलते हैं:
- आइसोलेशन (Isolation): वे आपको अपने कोड यूनिट (जैसे, फॉर्मेटिंग लॉजिक) को उसकी निर्भरता (जैसे, डेटाबेस) से पूरी तरह से अलग करके परीक्षण करने की अनुमति देते हैं। यदि परीक्षण विफल हो जाता है, तो आप जानते हैं कि समस्या परीक्षण के तहत यूनिट में है, कहीं और नहीं।
- गति (Speed): नेटवर्क अनुरोधों या डेटाबेस प्रश्नों जैसे धीमे ऑपरेशनों को इन-मेमोरी टेस्ट डबल से बदलने से आपका टेस्ट सूट नाटकीय रूप से तेजी से चलता है। तेज टेस्ट अधिक बार चलाए जाते हैं, जिससे डेवलपर्स के लिए एक मजबूत फीडबैक लूप बनता है।
- नियतात्मकता (Determinism): आप टेस्ट डबल को हर बार टेस्ट चलाने पर अनुमानित डेटा वापस करने के लिए कॉन्फ़िगर कर सकते हैं। यह फ्लेकी टेस्ट को समाप्त करता है और यह सुनिश्चित करता है कि एक असफल टेस्ट एक वास्तविक समस्या को इंगित करता है।
- एज केस का परीक्षण करने की क्षमता: आप त्रुटि स्थितियों का अनुकरण करने के लिए एक डबल को आसानी से कॉन्फ़िगर कर सकते हैं, जैसे कि
ConnectionErrorउठाना या खाली डेटा लौटाना, जिससे आप यह सत्यापित कर सकते हैं कि आपका कोड इन स्थितियों को शालीनता से संभालता है।
टेस्ट डबल्स का वर्गीकरण: केवल "मॉक्स" से परे
जबकि डेवलपर्स अक्सर किसी भी टेस्ट डबल को संदर्भित करने के लिए "मॉक" शब्द का सामान्य रूप से उपयोग करते हैं, जेरार्ड मेस्जारोस द्वारा अपनी पुस्तक "xUnit Test Patterns" में गढ़ी गई अधिक सटीक शब्दावली को समझना सहायक होता है। इन भेदों को जानने से आपको यह सोचने में मदद मिलती है कि आप अपने टेस्ट में क्या हासिल करने की कोशिश कर रहे हैं।
1. डमी (Dummy)
एक डमी ऑब्जेक्ट सबसे सरल टेस्ट डबल है। इसे एक पैरामीटर सूची भरने के लिए पास किया जाता है लेकिन वास्तव में इसका कभी उपयोग नहीं किया जाता है। इसके तरीकों को आम तौर पर नहीं बुलाया जाता है। आप एक डमी का उपयोग तब करते हैं जब आपको किसी विधि को एक तर्क प्रदान करने की आवश्यकता होती है, लेकिन आप उस विशिष्ट परीक्षण के संदर्भ में उस तर्क के व्यवहार की परवाह नहीं करते हैं।
उदाहरण: यदि किसी फ़ंक्शन को 'लॉगर' ऑब्जेक्ट की आवश्यकता होती है, लेकिन आपका टेस्ट इस बात से संबंधित नहीं है कि क्या लॉग किया गया है, तो आप एक डमी ऑब्जेक्ट पास कर सकते हैं।
2. फेक (Fake)
एक फेक ऑब्जेक्ट का एक काम करने वाला कार्यान्वयन होता है, लेकिन यह प्रोडक्शन ऑब्जेक्ट का एक बहुत ही सरल संस्करण होता है। यह बाहरी संसाधनों का उपयोग नहीं करता है और एक भारी-भरकम कार्यान्वयन के लिए एक हल्के कार्यान्वयन को प्रतिस्थापित करता है। इसका क्लासिक उदाहरण एक इन-मेमोरी डेटाबेस है जो एक वास्तविक डेटाबेस कनेक्शन की जगह लेता है। यह वास्तव में काम करता है—आप इसमें डेटा जोड़ सकते हैं और इससे डेटा पढ़ सकते हैं—लेकिन यह बस हुड के नीचे एक सरल डिक्शनरी या सूची है।
3. स्टब (Stub)
एक स्टब एक टेस्ट के दौरान किए गए मेथड कॉल्स को पूर्व-क्रमादेशित, "डिब्बाबंद" उत्तर प्रदान करता है। इसका उपयोग तब किया जाता है जब आपको अपने कोड को किसी निर्भरता से विशिष्ट डेटा प्राप्त करने की आवश्यकता होती है। उदाहरण के लिए, आप api_client.get_user(user_id=123) जैसे मेथड को हमेशा एक विशिष्ट उपयोगकर्ता डिक्शनरी लौटाने के लिए स्टब कर सकते हैं, बिना वास्तव में एपीआई कॉल किए।
4. स्पाई (Spy)
एक स्पाई एक स्टब है जो इस बारे में भी कुछ जानकारी रिकॉर्ड करता है कि इसे कैसे बुलाया गया था। उदाहरण के लिए, यह रिकॉर्ड कर सकता है कि किसी मेथड को कितनी बार बुलाया गया था या उसे कौन से तर्क दिए गए थे। यह आपको अपने कोड और उसकी निर्भरता के बीच की बातचीत की "जासूसी" करने और फिर उस बातचीत के बारे में बाद में दावे करने की अनुमति देता है।
5. मॉक (Mock)
मॉक सबसे 'जागरूक' प्रकार का टेस्ट डबल है। यह एक ऐसा ऑब्जेक्ट है जो अपेक्षाओं के साथ पूर्व-क्रमादेशित है कि कौन से मेथड बुलाए जाएंगे, किन तर्कों के साथ, और किस क्रम में। मॉक ऑब्जेक्ट का उपयोग करने वाला एक टेस्ट आम तौर पर न केवल तब विफल होगा जब परीक्षण के तहत कोड गलत परिणाम उत्पन्न करता है, बल्कि तब भी जब यह मॉक के साथ ठीक अपेक्षित तरीके से इंटरैक्ट नहीं करता है। मॉक्स व्यवहार सत्यापन (behavior verification) के लिए बहुत अच्छे हैं—यह सुनिश्चित करना कि क्रियाओं का एक विशिष्ट क्रम हुआ।
पाइथन की unittest.mock लाइब्रेरी एक एकल, शक्तिशाली क्लास प्रदान करती है जो आपके उपयोग के तरीके के आधार पर स्टब, स्पाई या मॉक के रूप में कार्य कर सकती है।
पेश है पाइथन का पावरहाउस: `unittest.mock` लाइब्रेरी
पाइथन 3.3 संस्करण के बाद से पाइथन की मानक लाइब्रेरी का हिस्सा, unittest.mock टेस्ट डबल्स बनाने के लिए एक प्रामाणिक समाधान है। इसका लचीलापन और शक्ति इसे किसी भी गंभीर पाइथन डेवलपर के लिए एक आवश्यक उपकरण बनाती है। यदि आप पाइथन के पुराने संस्करण का उपयोग कर रहे हैं, तो आप पिप के माध्यम से बैकपोर्टेड लाइब्रेरी स्थापित कर सकते हैं: pip install mock।
लाइब्रेरी का कोर दो प्रमुख क्लासों के इर्द-गिर्द घूमता है: Mock और इसकी अधिक सक्षम सहोदर, MagicMock। इन ऑब्जेक्ट्स को अविश्वसनीय रूप से लचीला होने के लिए डिज़ाइन किया गया है, जो आपके द्वारा एक्सेस करते ही फ्लाई पर एट्रिब्यूट्स और मेथड्स बनाते हैं।
गहन विश्लेषण: `Mock` और `MagicMock` क्लासेस
`Mock` ऑब्जेक्ट
एक `Mock` ऑब्जेक्ट एक गिरगिट की तरह है। आप एक बना सकते हैं, और यह तुरंत किसी भी एट्रिब्यूट एक्सेस या मेथड कॉल का जवाब देगा, डिफ़ॉल्ट रूप से एक और मॉक ऑब्जेक्ट लौटाएगा। यह आपको सेटअप के दौरान आसानी से कॉल्स को एक साथ श्रृंखलाबद्ध करने की अनुमति देता है।
# In a test file...
from unittest.mock import Mock
# Create a mock object
mock_api = Mock()
# Accessing an attribute creates it and returns another mock
print(mock_api.users)
# Output: <Mock name='mock.users' id='...'>
# Calling a method also returns a mock by default
print(mock_api.users.get(id=1))
# Output: <Mock name='mock.users.get()' id='...'>
यह डिफ़ॉल्ट व्यवहार परीक्षण के लिए बहुत उपयोगी नहीं है। असली शक्ति मॉक को उस ऑब्जेक्ट की तरह व्यवहार करने के लिए कॉन्फ़िगर करने से आती है जिसे वह बदल रहा है।
रिटर्न मान और साइड इफेक्ट्स को कॉन्फ़िगर करना
आप मॉक मेथड को बता सकते हैं कि return_value एट्रिब्यूट का उपयोग करके क्या लौटाना है। इस तरह आप एक स्टब (Stub) बनाते हैं।
from unittest.mock import Mock
# Create a mock for a data service
mock_service = Mock()
# Configure the return value for a method call
mock_service.get_data.return_value = {'id': 1, 'name': 'Test Data'}
# Now when we call it, we get our configured value
result = mock_service.get_data()
print(result)
# Output: {'id': 1, 'name': 'Test Data'}
त्रुटियों का अनुकरण करने के लिए, आप side_effect एट्रिब्यूट का उपयोग कर सकते हैं। यह आपके कोड की त्रुटि प्रबंधन (error handling) का परीक्षण करने के लिए एकदम सही है।
from unittest.mock import Mock
mock_service = Mock()
# Configure the method to raise an exception
mock_service.get_data.side_effect = ConnectionError("Failed to connect to service")
# Calling the method will now raise the exception
try:
mock_service.get_data()
except ConnectionError as e:
print(e)
# Output: Failed to connect to service
सत्यापन के लिए असर्शन मेथड्स
मॉक ऑब्जेक्ट्स यह रिकॉर्ड करके स्पाई (Spies) और मॉक्स (Mocks) के रूप में भी कार्य करते हैं कि उनका उपयोग कैसे किया जाता है। फिर आप इन इंटरैक्शन को सत्यापित करने के लिए अंतर्निहित असर्शन मेथड्स के एक सूट का उपयोग कर सकते हैं।
mock_object.method.assert_called(): दावा करता है कि मेथड को कम से कम एक बार बुलाया गया था।mock_object.method.assert_called_once(): दावा करता है कि मेथड को ठीक एक बार बुलाया गया था।mock_object.method.assert_called_with(*args, **kwargs): दावा करता है कि मेथड को आखिरी बार निर्दिष्ट तर्कों के साथ बुलाया गया था।mock_object.method.assert_any_call(*args, **kwargs): दावा करता है कि मेथड को किसी भी समय इन तर्कों के साथ बुलाया गया था।mock_object.method.assert_not_called(): दावा करता है कि मेथड को कभी नहीं बुलाया गया था।mock_object.call_count: एक पूर्णांक प्रॉपर्टी जो आपको बताती है कि मेथड को कितनी बार बुलाया गया था।
from unittest.mock import Mock
mock_notifier = Mock()
# Imagine this is our function under test
def process_and_notify(data, notifier):
if data.get('critical'):
notifier.send_alert(message="Critical event occurred!")
# Test case 1: Critical data
process_and_notify({'critical': True}, mock_notifier)
mock_notifier.send_alert.assert_called_once_with(message="Critical event occurred!")
# Reset the mock for the next test
mock_notifier.reset_mock()
# Test case 2: Non-critical data
process_and_notify({'critical': False}, mock_notifier)
mock_notifier.send_alert.assert_not_called()
`MagicMock` ऑब्जेक्ट
एक `MagicMock` `Mock` का एक उपवर्ग है जिसमें एक महत्वपूर्ण अंतर है: इसमें पाइथन के अधिकांश "मैजिक" या "डंडर" मेथड्स (जैसे, __len__, __str__, __iter__) के लिए डिफ़ॉल्ट कार्यान्वयन होते हैं। यदि आप एक नियमित `Mock` का उपयोग एक ऐसे संदर्भ में करने का प्रयास करते हैं जिसमें इनमें से किसी एक मेथड की आवश्यकता होती है, तो आपको एक त्रुटि मिलेगी।
from unittest.mock import Mock, MagicMock
# Using a regular Mock
mock_list = Mock()
try:
len(mock_list)
except TypeError as e:
print(e) # Output: 'Mock' object has no len()
# Using a MagicMock
magic_mock_list = MagicMock()
print(len(magic_mock_list)) # Output: 0 (by default)
# We can configure the magic method's return value too
magic_mock_list.__len__.return_value = 100
print(len(magic_mock_list)) # Output: 100
अंगूठे का नियम (Rule of thumb): `MagicMock` से शुरू करें। यह आम तौर पर सुरक्षित है और अधिक उपयोग के मामलों को कवर करता है, जैसे कि उन ऑब्जेक्ट्स को मॉक करना जो for लूप (__iter__ की आवश्यकता) या with स्टेटमेंट (__enter__ और __exit__ की आवश्यकता) में उपयोग किए जाते हैं।
व्यावहारिक कार्यान्वयन: `patch` डेकोरेटर और कॉन्टेक्स्ट मैनेजर
एक मॉक बनाना एक बात है, लेकिन आप अपने कोड को वास्तविक ऑब्जेक्ट के बजाय इसका उपयोग करने के लिए कैसे प्रेरित करते हैं? यहीं पर `patch` काम आता है। `patch`, `unittest.mock` में एक शक्तिशाली उपकरण है जो एक टेस्ट की अवधि के लिए एक लक्ष्य ऑब्जेक्ट को अस्थायी रूप से एक मॉक से बदल देता है।
डेकोरेटर के रूप में `@patch`
`patch` का उपयोग करने का सबसे आम तरीका आपके टेस्ट मेथड पर डेकोरेटर के रूप में है। आप उस ऑब्जेक्ट का स्ट्रिंग पथ प्रदान करते हैं जिसे आप बदलना चाहते हैं।
मान लीजिए हमारे पास एक फ़ंक्शन है जो लोकप्रिय `requests` लाइब्रेरी का उपयोग करके वेब एपीआई से डेटा प्राप्त करता है:
# in file: my_app/data_fetcher.py
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status() # Raise an exception for bad status codes
return response.json()
हम इस फ़ंक्शन का परीक्षण वास्तविक नेटवर्क कॉल किए बिना करना चाहते हैं। हम `requests.get` को पैच कर सकते हैं:
# in file: tests/test_data_fetcher.py
import unittest
from unittest.mock import patch, Mock
from my_app.data_fetcher import get_user_data
class TestDataFetcher(unittest.TestCase):
@patch('my_app.data_fetcher.requests.get')
def test_get_user_data_success(self, mock_get):
"""Test successful data fetching."""
# Configure the mock to simulate a successful API response
mock_response = Mock()
mock_response.json.return_value = {'id': 1, 'name': 'John Doe'}
mock_response.raise_for_status.return_value = None # Do nothing on success
mock_get.return_value = mock_response
# Call our function
user_data = get_user_data(1)
# Assert our function made the correct API call
mock_get.assert_called_once_with('https://api.example.com/users/1')
# Assert our function returned the expected data
self.assertEqual(user_data, {'id': 1, 'name': 'John Doe'})
ध्यान दें कि कैसे `patch` एक `MagicMock` बनाता है और इसे हमारे टेस्ट मेथड में `mock_get` तर्क के रूप में पास करता है। टेस्ट के भीतर, `my_app.data_fetcher` के अंदर `requests.get` पर कोई भी कॉल हमारे मॉक ऑब्जेक्ट पर पुनर्निर्देशित हो जाती है।
कॉन्टेक्स्ट मैनेजर के रूप में `patch`
कभी-कभी आपको किसी टेस्ट के केवल एक छोटे से हिस्से के लिए कुछ पैच करने की आवश्यकता होती है। इसके लिए `with` स्टेटमेंट के साथ `patch` को कॉन्टेक्स्ट मैनेजर के रूप में उपयोग करना एकदम सही है।
# in file: tests/test_data_fetcher.py
import unittest
from unittest.mock import patch, Mock
from my_app.data_fetcher import get_user_data
class TestDataFetcher(unittest.TestCase):
def test_get_user_data_with_context_manager(self):
"""Test using patch as a context manager."""
with patch('my_app.data_fetcher.requests.get') as mock_get:
# Configure the mock inside the 'with' block
mock_response = Mock()
mock_response.json.return_value = {'id': 2, 'name': 'Jane Doe'}
mock_get.return_value = mock_response
user_data = get_user_data(2)
mock_get.assert_called_once_with('https://api.example.com/users/2')
self.assertEqual(user_data, {'id': 2, 'name': 'Jane Doe'})
# Outside the 'with' block, requests.get is back to its original state
एक महत्वपूर्ण अवधारणा: कहाँ पैच करें?
यह `patch` का उपयोग करते समय भ्रम का सबसे आम स्रोत है। नियम है: आपको ऑब्जेक्ट को वहां पैच करना होगा जहां उसे देखा (looked up) जाता है, न कि वहां जहां उसे परिभाषित (defined) किया गया है।
आइए एक उदाहरण से स्पष्ट करें। मान लीजिए हमारे पास दो फाइलें हैं:
# in file: services.py
class Database:
def connect(self):
# ... complex connection logic ...
return "REAL_CONNECTION"
# in file: main_app.py
from services import Database
def start_app():
db = Database()
connection = db.connect()
print(f"Got connection: {connection}")
return connection
अब, हम `main_app.py` में `start_app` का परीक्षण करना चाहते हैं, बिना वास्तविक `Database` ऑब्जेक्ट बनाए। एक आम गलती `services.Database` को पैच करने का प्रयास करना है।
# in file: test_main_app.py
import unittest
from unittest.mock import patch
from main_app import start_app
class TestApp(unittest.TestCase):
# THIS IS THE WRONG WAY TO PATCH!
@patch('services.Database')
def test_start_app_incorrectly(self, mock_db):
start_app()
# This test will still use the REAL Database class!
# THIS IS THE CORRECT WAY TO PATCH!
@patch('main_app.Database')
def test_start_app_correctly(self, mock_db_class):
# We are patching 'Database' in the 'main_app' namespace
# Configure the mock instance that will be created
mock_instance = mock_db_class.return_value
mock_instance.connect.return_value = "MOCKED_CONNECTION"
connection = start_app()
# Assert that our mock was used
mock_db_class.assert_called_once() # Was the class instantiated?
mock_instance.connect.assert_called_once() # Was the connect method called?
self.assertEqual(connection, "MOCKED_CONNECTION")
पहला टेस्ट क्यों विफल होता है? क्योंकि `main_app.py`, `from services import Database` को निष्पादित करता है। यह `Database` क्लास को `main_app` मॉड्यूल के नेमस्पेस में आयात करता है। जब `start_app` चलता है, तो यह अपने स्वयं के मॉड्यूल (`main_app`) के भीतर `Database` की तलाश करता है। `services.Database` को पैच करने से यह `services` मॉड्यूल में बदल जाता है, लेकिन `main_app` के पास पहले से ही मूल क्लास का अपना संदर्भ है। सही तरीका `main_app.Database` को पैच करना है, जो वह नाम है जिसका परीक्षण के तहत कोड वास्तव में उपयोग करता है।
उन्नत मॉकिंग तकनीकें
`spec` और `autospec`: मॉक्स को सुरक्षित बनाना
एक मानक `MagicMock` का एक संभावित दोष है: यह आपको किसी भी तर्क के साथ किसी भी मेथड को कॉल करने की अनुमति देगा, भले ही वह मेथड वास्तविक ऑब्जेक्ट पर मौजूद न हो। इससे ऐसे टेस्ट हो सकते हैं जो पास हो जाते हैं लेकिन वास्तविक समस्याओं को छिपाते हैं, जैसे कि मेथड नामों में टाइपो या वास्तविक ऑब्जेक्ट के एपीआई में परिवर्तन।
# Real class
class Notifier:
def send_message(self, text):
# ... sends message ...
pass
# A test with a typo
from unittest.mock import MagicMock
mock_notifier = MagicMock()
# Oops, a typo! The real method is send_message
mock_notifier.send_mesage("hello") # No error is raised!
mock_notifier.send_mesage.assert_called_with("hello") # This assertion passes!
# Our test is green, but the production code would fail.
इसे रोकने के लिए, `unittest.mock` `spec` और `autospec` तर्क प्रदान करता है।
- `spec=SomeClass`: यह मॉक को `SomeClass` के समान एपीआई रखने के लिए कॉन्फ़िगर करता है। यदि आप किसी ऐसे मेथड या एट्रिब्यूट तक पहुंचने का प्रयास करते हैं जो वास्तविक क्लास पर मौजूद नहीं है, तो एक `AttributeError` उठाया जाएगा।
- `autospec=True` (या `autospec=SomeClass`): यह और भी अधिक शक्तिशाली है। यह `spec` की तरह काम करता है, लेकिन यह किसी भी मॉक्ड मेथड के कॉल सिग्नेचर की भी जाँच करता है। यदि आप किसी मेथड को गलत संख्या या तर्कों के नामों से कॉल करते हैं, तो यह एक `TypeError` उठाएगा, ठीक वैसे ही जैसे वास्तविक ऑब्जेक्ट करता।
from unittest.mock import create_autospec
# Create a mock that has the same interface as our Notifier class
spec_notifier = create_autospec(Notifier)
try:
# This will fail immediately because of the typo
spec_notifier.send_mesage("hello")
except AttributeError as e:
print(e) # Output: Mock object has no attribute 'send_mesage'
try:
# This will fail because the signature is wrong (no 'text' keyword)
spec_notifier.send_message("hello")
except TypeError as e:
print(e) # Output: missing a required argument: 'text'
# This is the correct way to call it
spec_notifier.send_message(text="hello") # This works!
spec_notifier.send_message.assert_called_once_with(text="hello")
सर्वोत्तम अभ्यास (Best practice): पैचिंग करते समय हमेशा `autospec=True` का उपयोग करें। यह आपके टेस्ट को अधिक मजबूत और कम भंगुर बनाता है। `@patch('path.to.thing', autospec=True)`।
वास्तविक-विश्व उदाहरण: एक डेटा प्रोसेसिंग सेवा का परीक्षण
आइए एक और पूर्ण उदाहरण के साथ सब कुछ एक साथ बांधें। हमारे पास एक `ReportGenerator` है जो एक डेटाबेस और एक फाइल सिस्टम पर निर्भर करता है।
# in file: app/services.py
class DatabaseConnector:
def get_sales_data(self, start_date, end_date):
# In reality, this would query a database
raise NotImplementedError("This should not be called in tests")
class FileSaver:
def save_report(self, path, content):
# In reality, this would write to a file
raise NotImplementedError("This should not be called in tests")
# in file: app/reports.py
from .services import DatabaseConnector, FileSaver
class ReportGenerator:
def __init__(self):
self.db_connector = DatabaseConnector()
self.file_saver = FileSaver()
def generate_sales_report(self, start_date, end_date, output_path):
"""Fetches sales data and saves a formatted report."""
raw_data = self.db_connector.get_sales_data(start_date, end_date)
if not raw_data:
report_content = "No sales data for this period."
else:
total_sales = sum(item['amount'] for item in raw_data)
report_content = f"Total Sales from {start_date} to {end_date}: ${total_sales:.2f}"
self.file_saver.save_report(path=output_path, content=report_content)
return True
अब, आइए `ReportGenerator.generate_sales_report` के लिए एक यूनिट टेस्ट लिखें जो इसकी निर्भरता को मॉक करता है।
# in file: tests/test_reports.py
import unittest
from datetime import date
from unittest.mock import patch, Mock
from app.reports import ReportGenerator
class TestReportGenerator(unittest.TestCase):
@patch('app.reports.FileSaver', autospec=True)
@patch('app.reports.DatabaseConnector', autospec=True)
def test_generate_sales_report_with_data(self, mock_db_connector_class, mock_file_saver_class):
"""Test report generation when the database returns data."""
# Arrange: Setup our mocks
mock_db_instance = mock_db_connector_class.return_value
mock_file_saver_instance = mock_file_saver_class.return_value
# Configure the database mock to return some fake data (Stub)
fake_data = [
{'id': 1, 'amount': 100.50},
{'id': 2, 'amount': 75.00},
{'id': 3, 'amount': 25.25}
]
mock_db_instance.get_sales_data.return_value = fake_data
start = date(2023, 1, 1)
end = date(2023, 1, 31)
path = '/reports/sales_jan_2023.txt'
# Act: Create an instance of our class and call the method
generator = ReportGenerator()
result = generator.generate_sales_report(start, end, path)
# Assert: Verify the interactions and results
# 1. Was the database called correctly?
mock_db_instance.get_sales_data.assert_called_once_with(start, end)
# 2. Was the file saver called with the correct, calculated content?
expected_content = "Total Sales from 2023-01-01 to 2023-01-31: $200.75"
mock_file_saver_instance.save_report.assert_called_once_with(
path=path,
content=expected_content
)
# 3. Did our method return the correct value?
self.assertTrue(result)
यह टेस्ट `generate_sales_report` के भीतर के लॉजिक को डेटाबेस और फाइल सिस्टम की जटिलताओं से पूरी तरह से अलग करता है, जबकि यह अभी भी सत्यापित करता है कि यह उनके साथ सही ढंग से इंटरैक्ट करता है।
प्रभावी मॉकिंग के लिए सर्वोत्तम अभ्यास
- मॉक्स को सरल रखें: एक टेस्ट जिसके लिए बहुत जटिल मॉक कॉन्फ़िगरेशन की आवश्यकता होती है, अक्सर एक संकेत ("टेस्ट स्मेल") होता है कि परीक्षण के तहत यूनिट बहुत जटिल है और शायद एकल जिम्मेदारी सिद्धांत (Single Responsibility Principle) का उल्लंघन कर रही है। प्रोडक्शन कोड को रीफैक्टर करने पर विचार करें।
- सहयोगियों को मॉक करें, सब कुछ नहीं: आपको केवल उन ऑब्जेक्ट्स को मॉक करना चाहिए जिनके साथ आपकी परीक्षण की जा रही यूनिट संचार करती है (उसके सहयोगी)। उस ऑब्जेक्ट को मॉक न करें जिसका आप परीक्षण कर रहे हैं।
- `autospec=True` को प्राथमिकता दें: जैसा कि उल्लेख किया गया है, यह आपके टेस्ट को अधिक मजबूत बनाता है यह सुनिश्चित करके कि मॉक का इंटरफ़ेस वास्तविक ऑब्जेक्ट के इंटरफ़ेस से मेल खाता है। यह रीफैक्टरिंग के कारण होने वाली समस्याओं को पकड़ने में मदद करता है।
- प्रति टेस्ट एक मॉक (आदर्श रूप से): एक अच्छा यूनिट टेस्ट एक ही व्यवहार या इंटरैक्शन पर केंद्रित होता है। यदि आप अपने आप को एक टेस्ट में कई अलग-अलग ऑब्जेक्ट्स को मॉक करते हुए पाते हैं, तो इसे कई, अधिक केंद्रित टेस्ट में विभाजित करना बेहतर हो सकता है।
- अपने दावों में विशिष्ट रहें: केवल `mock.method.assert_called()` की जाँच न करें। यह सुनिश्चित करने के लिए `assert_called_with(...)` का उपयोग करें कि इंटरैक्शन सही डेटा के साथ हुआ। यह आपके टेस्ट को अधिक मूल्यवान बनाता है।
- आपके टेस्ट दस्तावेज़ीकरण हैं: अपने टेस्ट और मॉक ऑब्जेक्ट्स के लिए स्पष्ट और वर्णनात्मक नामों का उपयोग करें (जैसे, `mock_api_client`, `test_login_fails_on_network_error`)। यह टेस्ट के उद्देश्य को अन्य डेवलपर्स के लिए स्पष्ट करता है।
निष्कर्ष
टेस्ट डबल्स केवल परीक्षण के लिए एक उपकरण नहीं हैं; वे परीक्षण योग्य, मॉड्यूलर और रखरखाव योग्य सॉफ्टवेयर डिजाइन करने का एक मौलिक हिस्सा हैं। वास्तविक निर्भरता को नियंत्रित विकल्पों से बदलकर, आप एक ऐसा टेस्ट सूट बना सकते हैं जो तेज, विश्वसनीय और आपके एप्लिकेशन के लॉजिक के हर कोने को सत्यापित करने में सक्षम हो।
पाइथन की unittest.mock लाइब्रेरी इन पैटर्न को लागू करने के लिए एक विश्व स्तरीय टूलकिट प्रदान करती है। `MagicMock`, `patch`, और `autospec` की सुरक्षा में महारत हासिल करके, आप वास्तव में अलग-थलग यूनिट टेस्ट लिखने की क्षमता को अनलॉक करते हैं। यह आपको आत्मविश्वास के साथ जटिल एप्लिकेशन बनाने का अधिकार देता है, यह जानते हुए कि आपके पास रिग्रेशन को पकड़ने और नई सुविधाओं को मान्य करने के लिए सटीक, लक्षित टेस्ट का एक सुरक्षा जाल है। तो आगे बढ़ें, पैचिंग शुरू करें, और आज ही अधिक मजबूत पाइथन एप्लिकेशन बनाएं।